﻿// ---------------------------------------------------------------------------------------------------------------------
//   Copyright (c) SRT Solutions 2009. All rights reserved.
// ---------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.IO;

using NUnit.Framework;

namespace SRTSolutions.Elevate.Guide._001_Sequences._002_WorkingWithSequences
{
    public class _001_LinqExtensions
    {
        [Test]
        public void Choose()
        {
            //given a sequence of numbers
            var numbers = 1.Through(10);

            //and a function that returns the square of a number if it's even
            Func<int, Option<int>> squareIfEven = number => number % 2 == 0 ? Option.Some(number * number) : Option<int>.None;

            //we can apply the function to each element of the sequence, returning only
            //the results that have a result
            var result = numbers.Choose(squareIfEven);

            var expected = Seq.Build(4, 16, 36, 64, 100);
            Assert.AreEqual(expected, result);
        }

        [Test]
        public void Chunk()
        {
            //given a sequence
            var sequence = 1.Through(20);

            //we can split the sequence into a sequence of "chunks" that are each a specific length
            var chunks = sequence.Chunk(5);

            var expectedChunks = 
                Seq.Build<IEnumerable<int>>( 
                    1.Through(5).ToList(),
                    6.Through(10).ToList(),
                    11.Through(15).ToList(),
                    16.Through(20).ToList());

            Assert.AreEqual(expectedChunks, chunks);
        }

        [Test]
        public void ChunkWithUnevenElements()
        {
            //given a sequence
            var sequence = 1.Through(20);

            //if we chunk it with a number that doesn't divide evenly
            var chunks = sequence.Chunk(9);

            //the last chunk will not contain the full number of elements
            var expectedChunks =
                Seq.Build<IEnumerable<int>>(
                    1.Through(9),
                    10.Through(18),
                    19.Through(20));

            Assert.AreEqual(expectedChunks, chunks);
        }

        [Test]
        public void ChunkBy()
        {
            //given a sequence
            var sequence = 
                1.Through(20)
                .Concat(1.Through(20))
                .Concat(1.Through(20));

            //we already saw that we can divide the sequence into chunks
            //of a given length, but if we want to split it into
            //chunks based on conditions, we can do that like this
            var chunks = sequence.Chunk(value => value > 5 && value < 10, value => value > 18);

            //the chunks will start when the start condition is met (inclusive)
            //and stop when the end condition is met (exclusive)
            var expectedChunks = 3.Times(ignored => 6.Through(18));
            Assert.AreEqual(expectedChunks, chunks);
        }

        [Test]
        public void ChunkByStartConditionNotMet()
        {
            //given a sequence
            var sequence = 1.Through(20);

            //if we chunk with a start condition that isn't met
            var chunks = sequence.Chunk(value => value < 0, value => value > 0);

            //we'll get an empty result sequence
            Assert.IsFalse(chunks.Any());
        }

        [Test]
        public void ChunkByEndConditionNotMet()
        {
            //given a sequence
            var sequence = 1.Through(20);

            //if we chunk with an end condition that isn't met
            var chunks = sequence.Chunk(value => value > 15, value => value > 200);

            //the chunk will just continue until the sequence runs out
            var expectedChunks = 1.Times(ignore => 16.Through(20));
            Assert.AreEqual(expectedChunks, chunks);
        }

        [Test]
        public void CopyTo()
        {
            //given a sequence
            var sequence = 1.Through(50);

            //If we want to copy it to an array. (Like when we're implemening ICollection)
            //We can do it like this. All the semantics of the ICollection CopyTo method
            //are matched.
            var result = new int[70];
            sequence.CopyTo(result, 20);
            Assert.AreEqual(sequence, result.Skip(20));
        }

        [Test]
        public void DoAllowsYouToAddASideEffectWhenASequenceIsEnumerated()
        {
            //given a sequence
            var sequence = 1.Through(10);

            //we can perform a side effect when the sequence is walked by
            //using Do
            int sum = 0;

            sequence
                .Do(x => sum += x)
                .ToList();

            Assert.That(sum, Is.EqualTo(sequence.Sum()));
        }

        [Test]
        public void TheSideEffectsFromDoAreDoneLazily()
        {
            //given a sequence
            var sequence = 1.Through(10);

            //Do will not walk the sequence until it is enumerated
            int sum = 0;

            sequence
                .Do(x => sum += x);

            Assert.That(sum, Is.EqualTo(0));
        }

        [Test]
        public void DoReturnsTheOriginalSequence()
        {
            //given a sequence
            var sequence = 1.Through(10);

            //Do will yield the original elements
            var result = sequence.Do(x => x = 100);

            Assert.That(result, Is.EqualTo(sequence));
        }

        [Test]
        public void FirstIndexOf()
        {
            //given a sequence
            var values = new[] { "a", "b", "c", "d" };

            //we can find the index of the first matching element
            var indexOfC = values.FirstIndexOf("c");

            Assert.AreEqual(2, indexOfC);
        }

        [Test]
        public void FirstIndexOfThrowsIfElementWasNotFound()
        {
            //if the target element isn't found
            var values = 0.Through(10);

            //IndexOf throws
            Assert.Throws<ArgumentException>(() => values.FirstIndexOf(11));
        }

        [Test]
        public void FirstIndexWhere()
        {
            //given a sequence
            IEnumerable<int> values = 0.Through(4);

            //you can also use a predicate to find the index of an element
            var index = values.FirstIndexWhere(x => x > 3);

            Assert.AreEqual(4, index);
        }

        private bool walked;

        IEnumerable<int> ValuesWithHitTest()
        {
            walked = true;

            yield return 0;
            yield return 1;
            yield return 2;
            yield return 3;
            yield return 4;
            yield return 5;
            yield return 6;
            yield return 7;
            yield return 8;
            yield return 9;
        }

        [Test]
        public void Flatten()
        {
            //given an enumeration of enumerations
            var enumerations =
                Seq.Build<IEnumerable<int>>(
                    1.Through(3),
                    4.Through(5),
                    6.Through(10));

            //we can transform it into one sequence like this
            var flattened = enumerations.Flatten();

            Assert.AreEqual(1.Through(10), flattened);
        }

        [Test]
        public void ForEach()
        {
            //given a sequence of values
            var values = ValuesWithHitTest();

            //initially the sequence will not be walked
            Assert.IsFalse(walked);

            //but we can walk it with foreach
            var total = 0;
            values.ForEach(value => total += value);
            Assert.AreEqual(45, total);
            Assert.IsTrue(walked);
        }

        [Test]
        public void ForEachWithIndex()
        {
            //given a sequence of values
            var values = Seq.Build(10, 20, 30);

            //we can perfom an action on each item in the list using it's index like this
            using (var stream = new MemoryStream())
            using(var writer = new StreamWriter(stream))
            {
                values.ForEachWithIndex((index, value) => writer.WriteLine(string.Format("{0},{1}", index, value)));

                //verify that the results were as expected
                writer.Flush();
                stream.Position = 0;

                const string expected = "0,10\r\n1,20\r\n2,30\r\n";
                Assert.AreEqual(expected, new StreamReader(stream).ReadToEnd());
            }
        }

        IEnumerable<int> GetInfiniteSequence()
        {
            for (int i = 0; ; i++)
            {
                yield return i;
            }
        }

        [Test]
        public void TryFind()
        {
            //given a sequence
            var values = 0.Through(10);

            //We can detect the presense of an item and project the result
            //by using TryFind.
            //
            //Let's say we want to check if a number greater than 5 is in the list.
            //when the item is present, it's just like the LINQ function .First()
            Option<int> result = values.TryFind(x => x == 5);

            Assert.IsTrue(result.IsSome);
            Assert.AreEqual(5, result.Value);
        }

        [Test]
        public void IsDistinct()
        {
            // IsDistinct makes more sense with a more complex data type
            // lets use an anonymous type with properties Name and Value
            var things = new[] { new { Name="foo", Value=1},
                new {Name="bar", Value=2},
                new {Name="baz", Value=3}};
            var morethings = new[] { new {Name="foo",Value=1},
                new{Name="foo",Value=2}};
            var stillmorethings = new [] { new {Name="foo",Value=1},
                new{Name="bar",Value=1}};
            // IsDictinct tells us if an enumeration is distinct in a selector
            // This is usually a property
            Assert.IsTrue(things.IsDistinct(a => a.Name));
            Assert.IsTrue(things.IsDistinct(a => a.Value));
            Assert.IsFalse(morethings.IsDistinct(a => a.Name));
            Assert.IsTrue(morethings.IsDistinct(a => a.Value));
            Assert.IsTrue(stillmorethings.IsDistinct(a => a.Name));
            Assert.IsFalse(stillmorethings.IsDistinct(a => a.Value));
        }

        [Test]
        public void OptionTypes()
        {
            Option<int> optionWithValue = Option.Some(10);
            Assert.IsTrue(optionWithValue.IsSome);
            Assert.AreEqual(10, optionWithValue.Value);

            Option<int> optionWithoutValue = Option<int>.None;
            Assert.IsFalse(optionWithoutValue.IsSome);
        }

        [Test]
        public void TryFindOnFailure()
        {
            //given a sequence
            var values = 0.Through(10);

            //if we TryFind a condition that doesn't match
            Option<int> result = values.TryFind(x => x == 20);

            //we won't throw like .First will, but we will
            //be able to verify that no item matched.
            Assert.IsTrue(result.IsNone);
        }

        [Test]
        public void TryFirstIndexOf()
        {
            //given a sequence
            var values = new[] { "a", "b", "c", "d" };

            //we can find the index of the first matching element
            var indexOfC = values.TryFirstIndexOf("c");

            Assert.IsTrue(indexOfC.IsSome);
            Assert.AreEqual(2, indexOfC.Value);
        }

        [Test]
        public void TryFirstIndexOfReturnsNoneIfElementWasNotFound()
        {
            //if the target element isn't found
            var values = 0.Through(10);
            var index = values.TryFirstIndexOf(11);

            //None is returned
            Assert.IsTrue(index.IsNone);
        }

        [Test]
        public void TryFirstIndexWhere()
        {
            //given a sequence
            IEnumerable<int> values = 0.Through(4);

            //you can also use a predicate to try to find the index of an element
            var index = values.TryFirstIndexWhere(x => x > 3);

            Assert.IsTrue(index.IsSome);
            Assert.AreEqual(4, index.Value);
        }

        [Test]
        public void TryPick()
        {
            //given a sequence
            var values = 0.Through(15);

            //we can detect the presense of an item, and select a result by using TryPick
            //Here, let's say we want to see if the number 10 is in the list, and select 10's .ToString()
            Option<string> result = 
                values
                .TryPick(value => value == 10 ? Option.Some(value.ToString()) : Option<string>.None);

            Assert.AreEqual("10", result.Value);
        }

        [Test]
        public void TryPickOnFailure()
        {
            //given a sequence
            var values = 0.Through(15);

            //if our condition is not met, None is returned
            var result = 
                values
                .TryPick(value => value == 9999999 ? Option.Some(value.ToString()) : Option<string>.None);

            Assert.IsTrue(result.IsNone);
        }

        [Test]
        public void PairwiseSelect()
        {
            //given a sequence
            var values = 0.Through(5);

            //we can walk the list two elements at a time by using pairwise
            //and find the sum of the two elements
            var result = 
                values
                .Pairwise((x, y) => x+y);

            Assert.That(result, Is.EqualTo(new[] { 1, 3, 5, 7, 9 }));
        }

        [Test]
        public void PairwiseSelectReturnsEmptyIfThereIsOneElement()
        {
            //if there is only one element in the input sequence
            var values = new[] { 0 };

            //pairwise returns an empty sequence
            var result = 
                values
                .Pairwise((x, y) => x + y);

            Assert.That(result, Is.Empty);
        }

        [Test]
        public void PairwiseIterateExecutesActionsInsteadOfSelecting()
        {
            //given a sequence
            var values = 0.Through(5);

            //we can also use pairwise to execute an action while
            //walking a sequence two elements at a time
            Tuple<int, int> lastValue = null;
            values.Pairwise((x, y) => { lastValue = Tuple.Create(x, y); });

            Assert.That(lastValue.First, Is.EqualTo(4));
            Assert.That(lastValue.Second, Is.EqualTo(5));
        }

        [Test]
        public void Scan()
        {
            var values = 1.Through(10);
            var result = values.Scan((a, b) => a + b, 0);

            Assert.That(result.ToArray(), Is.EquivalentTo(new[] { 0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55 }));
        }

        [Test]
        public void IsNullOrEmptyDetectsEmptyEnumerable()
        {
            var sequence = Enumerable.Empty<int>();
            var result = sequence.IsNullOrEmpty();

            Assert.That(result, Is.True);
        }

        [Test]
        public void IsNullOrEmptyDetectsNull()
        {
            IEnumerable<double> sequence = null;
            var result = sequence.IsNullOrEmpty();

            Assert.That(result, Is.True);
        }

        [Test]
        public void IsNullOrEmptySpecializesOnCollection()
        {
            var empty = (ICollection<int>)new Collection<int>();
            var result = empty.IsNullOrEmpty();

            Assert.That(result, Is.True);
        }


        #region Unit Tests

        [Test]
        public void ChooseThisException()
        {
            IEnumerable<int> tmp = null;
            Assert.Throws<ArgumentNullException>(() => tmp.Choose(value => Option.Some(value)));
        }

        [Test]
        public void ChooseArgumentException()
        {
            var sequence = Seq.Build(1, 2, 3, 4);
            Assert.Throws<ArgumentNullException>(() => sequence.Choose<int, int>(null));
        }

        [Test]
        public void ChunkThisException()
        {
            Assert.Throws<ArgumentNullException>(() => ((IEnumerable<int>)null).Chunk(5));
        }

        [Test]
        public void ChunkZeroException()
        {
            var sequence = 1.Through(20);
            Assert.Throws<ArgumentException>(() => sequence.Chunk(0));
        }

        [Test]
        public void ChunkNegativeException()
        {
            var sequence = 1.Through(20);
            Assert.Throws<ArgumentException>(() => sequence.Chunk(-10));
        }

        [Test]
        public void ChunkByThisException()
        {
            Assert.Throws<ArgumentNullException>(()=> ((IEnumerable<int>)null).Chunk(x=>true, x=>true));
        }

        [Test]
        public void ChunkByNullStartException()
        {
            Assert.Throws<ArgumentNullException>(()=> 1.Through(10).Chunk(null, x=>true));
        }

        [Test]
        public void ChunkByNullStopException()
        {
            Assert.Throws<ArgumentNullException>(()=> 1.Through(10).Chunk(x=>true, null));
        }

        [Test]
        public void ChunkByEmptySequence()
        {
            var sequence = Seq.Build<int>();

            var result = sequence.Chunk(x => true, x => true);
            Assert.IsFalse(result.Any());
        }

        [Test]
        public void CopyToThisNull()
        {
            IEnumerable<int> tmp = null;
            var array = new int[15];
            Assert.Throws<ArgumentNullException>(() => tmp.CopyTo(array, 0));
        }

        [Test]
        public void CopyToArrayNull()
        {
            int[] array = null;
            Assert.Throws<ArgumentNullException>(() => Seq.Build<int>().CopyTo(array, 0));
        }

        [Test]
        public void CopyToIndexNegative()
        {
            var array = new int[10];
            Assert.Throws<ArgumentOutOfRangeException>(() => Seq.Build<int>().CopyTo(array, -1));
        }

        [Test]
        public void CopyToIndexGreaterThanArrayLength()
        {
            var array = new int[10];
            Assert.Throws<ArgumentException>(() => Seq.Build<int>().CopyTo(array, 15));
        }

        [Test]
        public void CopyToIndexEqualToArrayLength()
        {
            var array = new int[10];
            Assert.Throws<ArgumentException>(() => Seq.Build<int>().CopyTo(array, 10));
        }

        [Test]
        public void CopyToArrayTooSmall()
        {
            var sequence = 1.Through(10);
            var array = new int[10];
            Assert.Throws<ArgumentException>(() => sequence.CopyTo(array, 1));
            Assert.AreEqual(10.Times(ignored => 0), array);
        }

        [Test]
        public void CopyToEmptySequence()
        {
            IEnumerable<int> sequence = new int[] { };
            var array = new int[0];
            sequence.CopyTo(array, 0);

            Assert.AreEqual(0, array.Length);
            Assert.AreEqual(0, sequence.Count());
        }

        [Test]
        public void DoThisException()
        {
            IEnumerable<int> sequence = null;
            Assert.Throws<ArgumentNullException>(() => sequence.Do(x => x = 100));
        }

        [Test]
        public void DoArgumentException()
        {
            var sequence = 0.Through(10);
            Action<int> action = null;
            Assert.Throws<ArgumentNullException>(() => sequence.Do(action));
        }

        [Test]
        public void FirstIndexOfThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.FirstIndexOf(0));
        }

        [Test]
        public void FirstIndexWhereThrowsWhenPredicateDoesNotMatch()
        {
            var values = 0.Through(10);

            Assert.Throws<ArgumentException>(() => values.FirstIndexWhere(x => x == 12));
        }

        [Test]
        public void FirstIndexWhereThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.FirstIndexWhere(x => x == 12));
        }

        [Test]
        public void FirstIndexWherePredicateException()
        {
            var tmp = 0.Through(5);

            Assert.Throws<ArgumentNullException>(() => tmp.FirstIndexWhere(null));
        }

        [Test]
        public void FlattenThisException()
        {
            IEnumerable<IEnumerable<int>> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.Flatten());
        }

        [Test]
        public void ForEachThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.ForEach(item => item++));
        }

        [Test]
        public void ForEachArgumentException()
        {
            IEnumerable<int> tmp = 0.Through(4);

            Assert.Throws<ArgumentNullException>(() => tmp.ForEach(null));
        }

        [Test]
        public void ForEachWithIndexThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.ForEachWithIndex((index, item) => item++));
        }

        [Test]
        public void ForEachWithIndexArgumentException()
        {
            IEnumerable<int> tmp = 0.Through(4);

            Assert.Throws<ArgumentNullException>(() => tmp.ForEach(null));
        }

        [Test]
        public void PairwiseSelectEmptySequence()
        {
            var values = new int[] { };
            var result = values.Pairwise((x, y) => x * y);
            Assert.That(result, Is.Empty);
        }

        [Test]
        public void PairwiseSelectThisException()
        {
            IEnumerable<int> tmp = null;
            Assert.That(() => tmp.Pairwise((x, y) => x + y), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void PairwiseSelectArgumentException()
        {
            var tmp = 0.Through(10);
            Func<int, int, string> selector = null;
            Assert.That(() => tmp.Pairwise(selector), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void PairwiseIterateDoesNothingIfThereIsOneElement()
        {
            var values = new[] { 0 };

            var executed = false;
            values.Pairwise((x, y) => { executed = true; });

            Assert.That(executed, Is.False);
        }

        [Test]
        public void PairwiseIterateEmptySequence()
        {
            var values = new int[] { };

            var executed = false;
            values.Pairwise((x, y) => { executed = true; });
            Assert.That(executed, Is.False);
        }

        [Test]
        public void PairwiseIterateThisException()
        {
            IEnumerable<int> tmp = null;
            Assert.That(() => tmp.Pairwise((x, y) => { }), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void PairwiseIterateArgumentException()
        {
            var tmp = 0.Through(10);
            Action<int, int> action = null;
            Assert.That(() => tmp.Pairwise(action), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void ScanThrowsWhenSourceIsNull()
        {
            IEnumerable<int> tmp = null;
            Assert.That(() => tmp.Scan((a, b) => 1 / ( a + b), -1), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void ScanThrowsWhenAccumulatorIsNull()
        {
            var tmp = 0.Through(10);
            Func<int, int, int> accumulator = null;
            Assert.That(() => tmp.Scan(accumulator, 0), Throws.InstanceOf<ArgumentNullException>());
        }

        [Test]
        public void TryFindThisException()
        {
            Assert.Throws<ArgumentNullException>(() => ((IEnumerable<int>)null).TryFind(x => true));
        }

        [Test]
        public void TryFindArgumentException()
        {
            Assert.Throws<ArgumentNullException>(() => 1.Through(10).TryFind(null));
        }

        [Test]
        public void TryFindEmpty()
        {
            var result = Seq.Build<int>().TryFind(x => true);
            Assert.IsTrue(result.IsNone);
        }

        [Test]
        public void TryFirstIndexOfThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.TryFirstIndexOf(0));
        }

        [Test]
        public void TryFirstIndexWhereReturnsNoneWhenPredicateDoesNotMatch()
        {
            var values = 0.Through(10);
            var result = values.TryFirstIndexWhere(x => x == 12);
            Assert.IsTrue(result.IsNone);
        }

        [Test]
        public void TryFirstIndexWhereThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.TryFirstIndexWhere(x => x == 12));
        }

        [Test]
        public void TryFirstIndexWherePredicateException()
        {
            var tmp = 0.Through(5);

            Assert.Throws<ArgumentNullException>(() => tmp.TryFirstIndexWhere(null));
        }

        [Test]
        public void TryPickThisException()
        {
            IEnumerable<int> tmp = null;
            Assert.Throws<ArgumentNullException>(() => tmp.TryPick(value => Option<int>.None));
        }

        [Test]
        public void TryPickArgumentException()
        {
            Assert.Throws<ArgumentNullException>(() => Seq.Build<int>().TryPick<int, int>(null));
        }

        [Test]
        public void TryPickEmptySequence()
        {
            var sequence = Seq.Build<int>();
            var result = sequence.TryPick(value => Option.Some(value));
            Assert.IsTrue(result.IsNone);
        }

        #endregion

        #region Obsolete Features

        [Test]
        public void IsEmpty()
        {
            //given an "infinite" sequence
            var values = GetInfiniteSequence();

            //we can test to see if it's is empty like this
            //NOTE: We don't have to walk the whole list to do this
#pragma warning disable 0618
            var result = values.IsEmpty(); // or use !values.Any() from System.Linq

            Assert.IsFalse(result);
        }

        [Test]
        public void IsEmptyFalse()
        {
            var tmp = Seq.Build<int>();
            Assert.IsTrue(tmp.IsEmpty());
        }

        [Test]
        public void IsEmptyThisException()
        {
            Assert.Throws<ArgumentNullException>(() => ((IEnumerable<int>)null).IsEmpty());
        }

        [Test]
        public void SelectWithIndex()
        {
            //given a sequence of values
            var values = 10.Through(50, 10);

            //we can apply a selector function to each element based on the element's value and it's index
            var result = values.SelectWithIndex((index, value) => value / (index + 1)); //or use Select<T,int,TResult> from System.Linq

            var expected = Seq.Build(10, 10, 10, 10, 10);
            Assert.AreEqual(expected, result);
        }

        [Test]
        public void SelectWithIndexThisException()
        {
            IEnumerable<int> tmp = null;

            Assert.Throws<ArgumentNullException>(() => tmp.SelectWithIndex((first, second) => first));
        }

        //NOTE: this behavior changes when using System.Linq's Select override!
        //[Test]
        //public void SelectWithIndexNullSelectorException()
        //{
        //    IEnumerable<int> tmp = 0.Through(3);
        //    Func<int, int, int> selector = null;

        //    Assert.Throws<ArgumentNullException>(() => tmp.SelectWithIndex(selector));
        //}

        #endregion
    }
}
